Перейти к основному содержимому

5.01. Циклы в JavaScript

Разработчику Архитектору

Циклы в JavaScript

Что такое цикл?

Цикл – многократно повторяемая процедура. Она может быть с известным количеством итераций и с неопределённым (бесконечным). То есть, либо мы знаем, когда начинать, сколько раз повторить, либо повторяем снова и снова, пока что-то не изменится.


for

for используется для определённого количества операций, с указанием начального значения, условия и шага каждой итерации:

for (начало; условие; шаг) {
// Тело цикла
}

Так, это похоже на функцию, где есть три выражения:

  • начало задаёт начальное состояние цикла. Обычно здесь объявляется и инициализируется счётчик, который выполняется один раз перед началом. В начале можно как объявить переменную, так и использовать существующую;
  • условие, которое проверяется перед каждой итерацией. Если это условие true (истинно), цикл выполняется, если false – цикл останавливается. Если условие пропустить, цикл будет выполняться бесконечно. Тут можно использовать любые логические выражения (<, >, <=, => и т.д.);
  • шаг, который выполняется после каждой итерации и меняет счётчик.

Пример:

for (let i = 0; i < 5; i++) {
console.log(i); // Выведет 0, 1, 2, 3, 4
}

Как это работает?

  • for – ключевое слово, указывающее на начало цикла;
  • let i = 0 – это начало, создание переменной с именем i и со значением 0;
  • i < 5 – это условие, и если i < 5 то будет true и тело цикла выполняется;
  • console.log(i) – это тело цикла, которое выводит в консоль значение переменной i;
  • i++ - инкремент, когда значение увеличивается на 1;
  • повторная проверка будет выполняться снова и снова;
  • если проверка true – цикл запускается снова, и снова выполняется тело.

Шаг — это выражение, которое изменяет значение переменной-счётчика на каждой итерации. Обычно он стоит на третьем месте в заголовке цикла for.

Как можно описать шаг?

ШагОписание
i++увеличивает i на 1
i--уменьшает i на 1
i += 2увеличивает i на 2
i -= 3уменьшает i на 3
i *= 2умножает i на 2
i /= 10делит i на 10

++ и -— это инкремент и декремент. Это специальные операторы для увеличения или уменьшения значения на 1. Бывают пост- и пре- операторы:

  • i++ это постинкремент;
  • ++i это преинкремент;
  • i-— это постдекремент;
  • --i это предекремент. Разница между пре- и постформами важна только если результат используется сразу.

Большинство этих операций (++, --, +=, -=, *=, /=) работают с числами , но JavaScript достаточно гибкий, и эти операторы могут работать и с другими типами данных.

Основное использование, конечно, числа, к примеру x+=3.

Со строками работает только +=, оператор работает как конкатенация строк:

let str = 'Привет';
str += ', мир!'; // 'Привет, мир!'

Но ++, --, -=, *= и т.д. со строками работать не будут корректно , так как JS попытается привести строку к числу, что может привести к NaN.

С объектами и массивами такие операции не имеют смысла. Поэтому грамотно подходите к выбору циклов.


forEach

Для работы с массивами лучше использовать forEach (в других языках он является полноценным циклом, тогда как в JavaScript это просто метод объектов-массивов), который озволяет перебрать все элементы массива , выполнив для каждого элемента заданную функцию (т.н. callback-функцию).

Он удобен и читабелен, особенно когда не нужно управлять индексами вручную, как в классическом for. Синтаксис у него таков:

array.forEach(function(element, index, array) {
// тело функции
});

Здесь мы вызываем функцию с параметрами:

  • element - текущий элемент массива, обязательный;
  • index - индекс текущего элемента (начинается с 0);
  • array - сам массив, по которому идёт перебор.

Пример:

const fruits = ['яблоко', 'банан', 'груша'];

fruits.forEach(function(fruit) {
console.log(fruit);
});

// Вывод:
// яблоко
// банан
// груша

Функцию можно сделать стрелочной:

const numbers = [1, 2, 3];

numbers.forEach(num => console.log(num * 2));

// Вывод:
// 2
// 4
// 6

forEach не мутирует (не изменяет) исходный массив, break здесь не нужен (он его просто не поддерживает), а код будет более читабельным. Так что можно сказать, что в JS есть цикл foreach, но он просто метод для работы с массивами, для перебора элементов.


while

while используется, когда количество итераций неизвестно. Код будет выполняться то тех пор, пока условие true.

let i = 0;

while (i < 5) {
console.log(i); // Выведет 0, 1, 2, 3, 4
i++;
}

В данном случае, пока i не будет равным 5, будет выводиться значение i в консоль. Принцип такой же, как и в for, однако он не включает в себя объявление и цикл – всё, что нужно выполнять, выведено в тело цикла, в том числе i++ (инкремент) – «Пока условие истинно, делай вот это».


do...while

do…while отличается от while тем, что тело цикла выполнится хотя бы один раз, даже если условие false.

let i = 0;

do {
console.log(i); // Выведет 0
i++;
} while (i < 0); // Условие false, но цикл сработал 1 раз

Здесь заметно, что цикл начинается с ключевого слова do и заканчивается while. Это значит, что принцип такой – «Сделай вот это. Повторяй, пока условие истинно».


Отличия циклов

Таблица отличий циклов

ЦиклКогда использоватьОсобенности
forИзвестно количество итерацийКомпактный синтаксис
whileНеизвестно количество итерацийМожет не выполниться ни разу
do…whileПроверка после итерацииВыполнится минимум 1 раз

Операции выполняются в рамках функций, а циклы и условные операторы – части функций, словно инструменты. Каждая функция выполняется в соответствии с её телом. В общем случае JS работает в одном потоке, и долгие операции могут замораживать страницу, поэтому надо разбивать тяжёлые задачи на части. Однако, в ряде случаев, браузер «зависает» именно при ожидании результата выполнения функции. Тут мы подходим к асинхронности.


Цикл for...in

Цикл for...in перебирает перечисляемые свойства объекта. Он проходит по ключам объекта, а не по значениям.

const user = {
name: "Джон",
age: 30,
email: "john@example.com"
};

for (let key in user) {
console.log(key); // "name", "age", "email"
console.log(user[key]); // "Джон", 30, "john@example.com"
}

Цикл for...in работает с любыми объектами:

const car = {
brand: "Toyota",
model: "Camry",
year: 2020,
color: "blue"
};

for (let property in car) {
console.log(`${property}: ${car[property]}`);
}
// brand: Toyota
// model: Camry
// year: 2020
// color: blue

Цикл перебирает свойства в произвольном порядке. Свойства наследуемые через прототип также включаются в перебор:

const animal = {
eats: true
};

const rabbit = {
jumps: true,
__proto__: animal
};

for (let prop in rabbit) {
console.log(prop); // "jumps", "eats"
}

Для проверки принадлежности свойства самому объекту используется метод hasOwnProperty:

for (let prop in rabbit) {
if (rabbit.hasOwnProperty(prop)) {
console.log(`Собственное свойство: ${prop}`);
}
}
// Собственное свойство: jumps

Цикл for...of

Цикл for...of перебирает итерируемые объекты. Он проходит по значениям, а не по ключам.

const colors = ["red", "green", "blue"];

for (let color of colors) {
console.log(color); // "red", "green", "blue"
}

Цикл работает с массивами, строками и другими итерируемыми объектами:

const text = "JavaScript";

for (let char of text) {
console.log(char);
}
// J, a, v, a, S, c, r, i, p, t

Цикл for...of удобен для работы с коллекциями:

const numbers = [1, 2, 3, 4, 5];
let sum = 0;

for (let num of numbers) {
sum += num;
}

console.log(sum); // 15

Цикл поддерживает работу с множествами и картами:

const uniqueNumbers = new Set([1, 2, 3, 4, 5]);

for (let num of uniqueNumbers) {
console.log(num);
}

const phoneBook = new Map([
["Джон", "555-1234"],
["Мария", "555-5678"]
]);

for (let [name, phone] of phoneBook) {
console.log(`${name}: ${phone}`);
}

Цикл for await...of

Цикл for await...of работает с асинхронными итераторами. Он позволяет обрабатывать последовательность промисов.

async function processItems() {
const items = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];

for await (let item of items) {
console.log(item); // 1, 2, 3
}
}

Цикл полезен для последовательной обработки асинхронных операций:

async function fetchUsers() {
const userIds = [1, 2, 3, 4, 5];

for await (let id of userIds) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
console.log(user.name);
}
}

Цикл работает с асинхронными генераторами:

async function* asyncGenerator() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}

async function process() {
for await (let value of asyncGenerator()) {
console.log(value);
}
}

Управление потоком выполнения

break

Оператор break прекращает выполнение цикла. Выполнение программы продолжается со следующей инструкции после цикла.

for (let i = 0; i < 10; i++) {
if (i === 5) {
break;
}
console.log(i); // 0, 1, 2, 3, 4
}

Оператор работает во всех видах циклов:

let i = 0;

while (i < 10) {
if (i === 5) {
break;
}
console.log(i);
i++;
}

Метки позволяют выходить из вложенных циклов:

outerLoop: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break outerLoop;
}
console.log(`i=${i}, j=${j}`);
}
}

continue

Оператор continue пропускает текущую итерацию цикла. Выполнение переходит к следующей итерации.

for (let i = 0; i < 10; i++) {
if (i % 2 === 0) {
continue;
}
console.log(i); // 1, 3, 5, 7, 9
}

Оператор работает с любыми циклами:

let i = 0;

while (i < 10) {
i++;
if (i % 3 === 0) {
continue;
}
console.log(i); // 1, 2, 4, 5, 7, 8, 10
}

Метки работают с continue аналогично break:

outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (j === 1) {
continue outer;
}
console.log(`i=${i}, j=${j}`);
}
}

return

Оператор return завершает выполнение функции. Он возвращает значение из функции вызывающему коду.

function findValue(array, target) {
for (let item of array) {
if (item === target) {
return item;
}
}
return undefined;
}

const result = findValue([1, 2, 3, 4, 5], 3);
console.log(result); // 3

Оператор return прекращает выполнение функции немедленно:

function process(data) {
if (!data) {
return;
}

console.log("Обработка данных");
// Этот код не выполнится, если данные отсутствуют
}

В стрелочных функциях return может быть неявным:

const multiply = (a, b) => a * b;

const square = x => {
return x * x;
};

throw

Оператор throw создаёт исключение. Он прерывает нормальное выполнение программы.

function divide(a, b) {
if (b === 0) {
throw new Error("Деление на ноль невозможно");
}
return a / b;
}

try {
const result = divide(10, 0);
} catch (error) {
console.log(error.message); // "Деление на ноль невозможно"
}

Оператор работает с различными типами ошибок:

function validateAge(age) {
if (age < 0) {
throw new RangeError("Возраст не может быть отрицательным");
}
if (typeof age !== "number") {
throw new TypeError("Возраст должен быть числом");
}
return true;
}

Исключения можно создавать с собственными классами:

class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}

function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("Некорректный формат email");
}
return true;
}

Практические примеры

Поиск элемента в массиве:

function findUser(users, id) {
for (let user of users) {
if (user.id === id) {
return user;
}
}
throw new Error(`Пользователь с id ${id} не найден`);
}

Фильтрация данных:

function getActiveUsers(users) {
const result = [];

for (let user of users) {
if (!user.active) {
continue;
}
result.push(user);
}

return result;
}

Обработка вложенных структур:

function processMatrix(matrix) {
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] === null) {
break;
}
console.log(matrix[i][j]);
}
}
}

Асинхронная обработка:

async function processQueue(queue) {
for await (let task of queue) {
try {
await task.execute();
} catch (error) {
console.error("Ошибка выполнения задачи:", error);
continue;
}
}
}